package cn.akwangl.socket.nio;

import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Set;

/**
 * Socket NIO 模型测试.Client 端实现.
 * <p>
 * 模拟一个多人群聊功能实现.
 *
 * @author 阿坤同学
 * @date 2022/6/18 15:26
 * @since 0.0.6
 */
@Slf4j
public class NIOChatClient {

    private Charset charset = StandardCharsets.UTF_8;
    private SocketChannel serverChannel;
    private Selector selector;
    private ByteBuffer readBuffer = ByteBuffer.allocate(NIOChatService.DEFAULT_BUFFER_SIZE);
    private ByteBuffer writeBuffer = ByteBuffer.allocate(NIOChatService.DEFAULT_BUFFER_SIZE);

    public void start() {
        try {
            serverChannel = SocketChannel.open();
            serverChannel.configureBlocking(false);

            selector = Selector.open();

            serverChannel.register(selector, SelectionKey.OP_CONNECT);
            serverChannel.connect(new InetSocketAddress(NIOChatService.SERVER_ADDRESS, NIOChatService.DEFAULT_SERVER_PORT));
            while (true) {
                selector.select();

                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                for (SelectionKey selectionKey : selectionKeys) {
                    eventHandlers(selectionKey);
                }
                selectionKeys.clear();
            }
        } catch (IOException e) {
            log.error("server socket 连接失败.", e);
        } catch (ClosedSelectorException e) {
            // 用户正常退出 忽略
        } finally {
            close();
        }
    }

    /**
     * 事件监听处理器.
     * <p>
     * 当客户端与服务器连接时{@link SocketChannel#connect(SocketAddress)},会触发{@link SelectionKey#OP_CONNECT}事件,
     * 如果客户端与服务器已经连接就绪时,会将客户端设置为{@link SocketChannel#finishConnect()}状态,并且创建一个子线程来接收用户输入消息并
     * 发送给服务器,然后将Channel注册为{@link SelectionKey#OP_READ}事件.触发{@link SelectionKey#OP_READ}事件时,会接监听自服务器发给
     * 客户端消息,并打印到日志中.
     *
     * @param selectionKey 所有就绪Channel
     * @author 阿坤同学
     * @date 2022/6/23 22:23
     * @since 0.0.6
     */
    private void eventHandlers(SelectionKey selectionKey) throws IOException {
        if (selectionKey.isConnectable()) {
            SocketChannel serverChannel = (SocketChannel) selectionKey.channel();

            if (serverChannel.isConnectionPending()) {
                serverChannel.finishConnect();
                // 接收用户数据
                new Thread(new UserInputHandler(this)).start();
            }
            serverChannel.register(selector, SelectionKey.OP_READ);
        } else if (selectionKey.isReadable()) {
            SocketChannel serverChannel = (SocketChannel) selectionKey.channel();
            String serverMessage = receive(serverChannel);
            if (serverMessage.isEmpty()) {
                close();
            } else {
                log.info("客户端接收到消息:{}", serverMessage);
            }
        }
    }

    private String receive(SocketChannel serverChannel) throws IOException {
        readBuffer.clear();
        while (serverChannel.read(readBuffer) > 0) ;
        readBuffer.flip();
        return String.valueOf(charset.decode(readBuffer));
    }

    public void sendMessage(String userMsg) throws IOException {
        if (userMsg.isEmpty()) {
            return;
        }

        writeBuffer.clear();
        writeBuffer.put(userMsg.getBytes(StandardCharsets.UTF_8));
        writeBuffer.flip();
        while (writeBuffer.hasRemaining()) {
            serverChannel.write(writeBuffer);
        }

        if (userMsg.equals(NIOChatService.QUIT)) {
            selector.close();
        }
    }

    public void close() {
        try {
            serverChannel.close();
            selector.close();
        } catch (IOException e) {
            log.error("客户端关闭资源失败", e);
        }
    }

    public static void main(String[] args) {
        NIOChatClient chatClient = new NIOChatClient();
        chatClient.start();
    }
}
